/*
 * Copyright (c) 2023 Shenzhen Kaihong Digital Industry Development Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "se_napi_oma_session.h"

#include <memory>
#include "se_napi_oma_common.h"
#include "se_napi_oma_reader.h"
#include "se_napi_oma_channel.h"
#include "common_utils.h"

namespace OHOS {
namespace SE {
namespace OMAPI {

thread_local napi_ref NapiSEOmaSession::consRef_ = nullptr;

napi_value JSOmaSessionClassConstructor(napi_env env, napi_callback_info info)
{
    DebugLog("Enter function JSOmaSessionClassConstructor");
    napi_value thisVar = nullptr;
    napi_get_cb_info(env, info, nullptr, nullptr, &thisVar, nullptr);
    NapiSEOmaSession* omaJSClass = new NapiSEOmaSession();

    napi_status status = napi_wrap(
        env,
        thisVar,
        omaJSClass,
        [](napi_env env, void *data, void *hint) {
            NapiSEOmaSession *service = (NapiSEOmaSession *)data;
            if (service) {
                delete service;
                service = nullptr;
            }
        },
        nullptr,
        nullptr);
    if (status != napi_ok) {
        if (omaJSClass != nullptr) {
            delete omaJSClass;
            omaJSClass = nullptr;
        }
        ErrorLog("napi_wrap failed, object is null. status = %{public}d", status);
        return NapiSEOmaCommon::CreateUndefined(env);
    }
    return thisVar;
}

void NapiSEOmaSession::DefineSEOmaSessionJsClass(napi_env env, napi_value exports)
{
    DebugLog("Enter function DefineSEOmaSessionJsClass");
    napi_property_descriptor desc[] = {
        DECLARE_NAPI_FUNCTION("getReader", NapiSEOmaSession::GetReader),
        DECLARE_NAPI_FUNCTION("getATR", NapiSEOmaSession::GetATR),
        DECLARE_NAPI_FUNCTION("close", NapiSEOmaSession::Close),
        DECLARE_NAPI_FUNCTION("isClosed", NapiSEOmaSession::IsClosed),
        DECLARE_NAPI_FUNCTION("closeChannels", NapiSEOmaSession::CloseChannels),
        DECLARE_NAPI_FUNCTION("openBasicChannel", NapiSEOmaSession::OpenBasicChannel),
        DECLARE_NAPI_FUNCTION("openLogicalChannel", NapiSEOmaSession::OpenLogicalChannel),
    };
    // define JS class SEOmaSession, JS_Constructor is the callback function
    napi_value constructor = nullptr;
    napi_define_class(env, "SEOmaSession", NAPI_AUTO_LENGTH, JSOmaSessionClassConstructor,
        nullptr, sizeof(desc)/sizeof(desc[0]), desc, &constructor);
    napi_create_reference(env, constructor, 1, &consRef_);
}

napi_value NapiSEOmaSession::GetReader(napi_env env, napi_callback_info info)
{
    DebugLog("Enter function NapiSEOmaSession::GetReader:");
    napi_value jsReaderObj = nullptr;
    napi_value thisVar = nullptr;
    NapiSEOmaSession *napiSession = nullptr;
    NAPI_CALL(env, napi_get_cb_info(env, info, 0, nullptr, &thisVar, nullptr));
    napi_status status = napi_unwrap(env, thisVar, reinterpret_cast<void **>(&napiSession));
    NAPI_ASSERT(env, status == napi_ok, "failed to get napiSession");
    if (napiSession == nullptr || napiSession->session_ == nullptr || napiSession->reader_ == nullptr) {
        ErrorLog("NapiSEOmaSession::GetReader napiSession nullptr!");
        return NapiSEOmaCommon::CreateUndefined(env);
    }

    // return jsReaderObj
    napi_value constructor = nullptr;
    napi_get_reference_value(env, NapiSEOmaReader::consRef_, &constructor);
    napi_new_instance(env, constructor, 0, nullptr, &jsReaderObj);
    status = napi_wrap(
        env, jsReaderObj, napiSession->reader_,
        [](napi_env env, void *data, void *hint) {
            ErrorLog("NapiSEOmaSession::GetReader Reader delete JsReader!");
            if (data) {
                NapiSEOmaReader* service = (NapiSEOmaReader*)data;
                if (service) {
                    delete service;
                    service = nullptr;
                }
            }
        },
        nullptr, nullptr);
    if (status != napi_ok) {
        if (napiSession->reader_ != nullptr) {
            delete napiSession->reader_;
            napiSession->reader_ = nullptr;
        }
        ErrorLog("napi_wrap failed, object is null. status = %{public}d", status);
        return NapiSEOmaCommon::CreateUndefined(env);
    }
    DebugLog("Session::GetReader success");
    return jsReaderObj;
}

napi_value NapiSEOmaSession::GetATR(napi_env env, napi_callback_info info)
{
    DebugLog("Enter function NapiSEOmaSession::GetATR:");
    napi_value result = nullptr;
    napi_value thisVar = nullptr;
    size_t argc = ARGV_NUM_0;
    napi_value argv[] = {nullptr};
    NAPI_CALL(env, napi_get_cb_info(env, info, &argc, argv, &thisVar, nullptr));
    NapiSEOmaCommon::CheckArgCountAndThrow(env, argc, ARGV_NUM_0);

    NapiSEOmaSession *napiSession = nullptr;
    napi_status status = napi_unwrap(env, thisVar, reinterpret_cast<void **>(&napiSession));
    NAPI_ASSERT(env, status == napi_ok, "failed to get napiSession");
    if (napiSession == nullptr || napiSession->session_ == nullptr) {
        ErrorLog("GetService napiSession nullptr!");
        NapiSEOmaCommon::CheckSEStatusCodeAndThrow(env, SE_ILLEGAL_STATE_ERROR, "GetATR");
        return NapiSEOmaCommon::CreateUndefined(env);
    }

    // get ATRs by call native method, throw exception when failed
    std::string atr = "";
    int statusCode = napiSession->session_->GetATR(atr);
    NapiSEOmaCommon::CheckSEStatusCodeAndThrow(env, statusCode, "GetATR");
    NapiSEOmaCommon::ConvertStringToNumberArray(env, result, atr);
    DebugLog("Session::GetATR: %{public}lu", atr.length());
    return result;
}

napi_value NapiSEOmaSession::Close(napi_env env, napi_callback_info info)
{
    DebugLog("Enter function NapiSEOmaSession::Close:");
    napi_value thisVar = nullptr;
    size_t argc = ARGV_NUM_0;
    napi_value argv[] = {nullptr};
    NAPI_CALL(env, napi_get_cb_info(env, info, &argc, argv, &thisVar, nullptr));
    NapiSEOmaCommon::CheckArgCountAndThrow(env, argc, ARGV_NUM_0);

    NapiSEOmaSession *napiSession = nullptr;
    napi_status status = napi_unwrap(env, thisVar, reinterpret_cast<void **>(&napiSession));
    NAPI_ASSERT(env, status == napi_ok, "failed to get napiSession");
    if (napiSession == nullptr || napiSession->session_ == nullptr) {
        ErrorLog("GetService napiSession nullptr!");
        NapiSEOmaCommon::CheckSEStatusCodeAndThrow(env, SE_ILLEGAL_STATE_ERROR, "Close");
        return NapiSEOmaCommon::CreateUndefined(env);
    }

    // close session by call native method, throw exception when failed
    int statusCode = napiSession->session_->Close();
    napiSession->session_ = nullptr;
    NapiSEOmaCommon::CheckSEStatusCodeAndThrow(env, statusCode, "Close");
    DebugLog("Session::Close success");
    return NapiSEOmaCommon::CreateUndefined(env);
}

napi_value NapiSEOmaSession::IsClosed(napi_env env, napi_callback_info info)
{
    DebugLog("Enter function NapiSEOmaSession::IsClosed:");
    napi_value result = nullptr;
    napi_value thisVar = nullptr;
    size_t argc = ARGV_NUM_0;
    napi_value argv[] = {nullptr};
    bool isClose = true;
    NAPI_CALL(env, napi_get_cb_info(env, info, &argc, argv, &thisVar, nullptr));
    NapiSEOmaCommon::CheckArgCountAndThrow(env, argc, ARGV_NUM_0);

    NapiSEOmaSession *napiSession = nullptr;
    napi_status status = napi_unwrap(env, thisVar, reinterpret_cast<void **>(&napiSession));
    NAPI_ASSERT(env, status == napi_ok, "failed to get napiSession");
    if (napiSession == nullptr || napiSession->session_ == nullptr) {
        ErrorLog("GetService napiSession nullptr!");
        napi_get_boolean(env, isClose, &result);
        return result;
    }

    // retrieve isclose by call native method, throw exception when failed
    int statusCode = napiSession->session_->IsClosed(isClose);
    NapiSEOmaCommon::CheckSEStatusCodeAndThrow(env, statusCode, "IsClosed");
    napi_get_boolean(env, isClose, &result);
    DebugLog("Session::IsClosed: %{public}d", isClose);
    return result;
}

napi_value NapiSEOmaSession::CloseChannels(napi_env env, napi_callback_info info)
{
    DebugLog("Enter function NapiSEOmaSession::CloseChannels:");
    napi_value thisVar = nullptr;
    size_t argc = ARGV_NUM_0;
    napi_value argv[] = {nullptr};
    NAPI_CALL(env, napi_get_cb_info(env, info, &argc, argv, &thisVar, nullptr));
    NapiSEOmaCommon::CheckArgCountAndThrow(env, argc, ARGV_NUM_0);

    NapiSEOmaSession *napiSession = nullptr;
    napi_status status = napi_unwrap(env, thisVar, reinterpret_cast<void **>(&napiSession));
    NAPI_ASSERT(env, status == napi_ok, "failed to get napiSession");
    if (napiSession == nullptr || napiSession->session_ == nullptr) {
        ErrorLog("GetService napiSession nullptr!");
        NapiSEOmaCommon::CheckSEStatusCodeAndThrow(env, SE_ILLEGAL_STATE_ERROR, "CloseChannels");
        return NapiSEOmaCommon::CreateUndefined(env);
    }

    // close channels by call native method, throw exception when fail
    int statusCode = napiSession->session_->CloseChannels();
    NapiSEOmaCommon::CheckSEStatusCodeAndThrow(env, statusCode, "CloseChannels");
    DebugLog("Session::CloseChannels success");
    return NapiSEOmaCommon::CreateUndefined(env);
}

napi_value NapiSEOmaSession::OpenBasicChannel(napi_env env, napi_callback_info info)
{
    napi_value thisVar = nullptr;
    size_t argc = ARGV_NUM_3;
    napi_value argv[ARGV_NUM_3] = {nullptr};
    NAPI_CALL(env, napi_get_cb_info(env, info, &argc, argv, &thisVar, nullptr));
    NAPI_ASSERT(env, argc > ARGV_NUM_0, "parameter number error!");
    NAPI_ASSERT(env, argc <= ARGV_NUM_3, "parameter number error!");
    if (argc >= ARGV_NUM_2) {
        bool isTrue = NapiSEOmaCommon::IsTargetValueType(env, argv[ARGV_INDEX_1], napi_number) ||
        NapiSEOmaCommon::IsTargetValueType(env, argv[ARGV_INDEX_1], napi_function);
        NAPI_ASSERT(env, isTrue, "parameter type error!");
    }

    std::unique_ptr<SessionContext> asyncContext = std::make_unique<SessionContext>(env);
    if (!NapiSEOmaCommon::ParseBytesVector(env, asyncContext->aid, argv[ARGV_INDEX_0])) {
        ErrorLog("parse int vector failed");
        return NapiSEOmaCommon::CreateUndefined(env);
    }

    if (argc == ARGV_NUM_2) {
        if (NapiSEOmaCommon::IsTargetValueType(env, argv[ARGV_INDEX_1], napi_number)) {
            napi_get_value_int32(env, argv[ARGV_INDEX_1], &asyncContext->p2);
        } else {
            // callback function
            napi_create_reference(env, argv[ARGV_INDEX_1], 1, &(asyncContext->callbackRef));
        }
    } else if (argc == ARGV_NUM_3) {
            NAPI_ASSERT(env, NapiSEOmaCommon::IsTargetValueType(env, argv[ARGV_INDEX_1], napi_number),
                "parameter type error!");
            NAPI_ASSERT(env, NapiSEOmaCommon::IsTargetValueType(env, argv[ARGV_INDEX_2], napi_function),
                "parameter type error!");
            napi_get_value_int32(env, argv[ARGV_INDEX_1], &asyncContext->p2);
            napi_create_reference(env, argv[ARGV_INDEX_2], 1, &(asyncContext->callbackRef));
    }

    // open basic channel by call native method, throw exception when failed
    NapiSEOmaSession *napiSession = nullptr;
    napi_status status = napi_unwrap(env, thisVar, reinterpret_cast<void **>(&napiSession));
    NAPI_ASSERT(env, status == napi_ok, "failed to get napiSession");
    if (napiSession == nullptr || napiSession->session_ == nullptr) {
        ErrorLog("OpenBasicChannel napiSession nullptr!");
        NapiSEOmaCommon::CheckSEStatusCodeAndThrow(env, SE_ILLEGAL_STATE_ERROR, "OpenBasicChannel");
        return NapiSEOmaCommon::CreateUndefined(env);
    }
    asyncContext->napiSession = napiSession;
    return NapiSEOmaCommon::HandleAsyncWork(env, asyncContext.release(), "OpenBasicChannel",
        NativeOpenBasicChannel, NativeOpenBasicChannelCallBack);
}

napi_value NapiSEOmaSession::OpenLogicalChannel(napi_env env, napi_callback_info info)
{
    napi_value thisVar = nullptr;
    size_t argc = ARGV_NUM_3;
    napi_value argv[ARGV_NUM_3] = {nullptr};
    NAPI_CALL(env, napi_get_cb_info(env, info, &argc, argv, &thisVar, nullptr));
    DebugLog("OpenLogicalChannel argc = %{public}d", static_cast<int>(argc));
    NAPI_ASSERT(env, argc > ARGV_NUM_0, "parameter number error!");
    NAPI_ASSERT(env, argc <= ARGV_NUM_3, "parameter number error!");

    if (argc >= ARGV_NUM_2) {
        bool isTrue = NapiSEOmaCommon::IsTargetValueType(env, argv[ARGV_INDEX_1], napi_number) ||
        NapiSEOmaCommon::IsTargetValueType(env, argv[ARGV_INDEX_1], napi_function);
        NAPI_ASSERT(env, isTrue, "parameter type error!");
    }

    std::unique_ptr<SessionContext> asyncContext = std::make_unique<SessionContext>(env);
    if (!NapiSEOmaCommon::ParseBytesVector(env, asyncContext->aid, argv[ARGV_INDEX_0])) {
        ErrorLog("parse int vector failed");
        return NapiSEOmaCommon::CreateUndefined(env);
    }

    if (argc == ARGV_NUM_2) {
        if (NapiSEOmaCommon::IsTargetValueType(env, argv[ARGV_INDEX_1], napi_number)) {
            napi_get_value_int32(env, argv[ARGV_INDEX_1], &asyncContext->p2);
            DebugLog("argv: %{public}d", asyncContext->p2);
        } else {
            // callback function
            napi_create_reference(env, argv[ARGV_INDEX_1], 1, &(asyncContext->callbackRef));
            DebugLog("argv callback");
        }
    } else if (argc == ARGV_NUM_3) {
            NAPI_ASSERT(env, NapiSEOmaCommon::IsTargetValueType(env, argv[ARGV_INDEX_1], napi_number),
                "parameter type error!");
            NAPI_ASSERT(env, NapiSEOmaCommon::IsTargetValueType(env, argv[ARGV_INDEX_2], napi_function),
                "parameter type error!");
            napi_get_value_int32(env, argv[ARGV_INDEX_1], &asyncContext->p2);
            napi_create_reference(env, argv[ARGV_INDEX_2], 1, &(asyncContext->callbackRef));
            DebugLog("NapiSEOmaSession::OpenLogicalChannel argv number 3");
    }

    // open logic channel by call native method, throw exception when failed
    NapiSEOmaSession *napiSession = nullptr;
    napi_status status = napi_unwrap(env, thisVar, reinterpret_cast<void **>(&napiSession));
    NAPI_ASSERT(env, status == napi_ok, "failed to get napiSession");
    if (napiSession == nullptr || napiSession->session_ == nullptr) {
        ErrorLog("OpenLogicalChannel napiSession is nullptr!");
        NapiSEOmaCommon::CheckSEStatusCodeAndThrow(env, SE_ILLEGAL_STATE_ERROR, "OpenLogicalChannel");
        return NapiSEOmaCommon::CreateUndefined(env);
    }
    asyncContext->napiSession = napiSession;
    return NapiSEOmaCommon::HandleAsyncWork(env, asyncContext.release(), "openLogicalChannel",
        NativeOpenLogicChannel, NativeOpenLogicChannelCallBack);
}

void NapiSEOmaSession::NativeOpenBasicChannel(napi_env env, void *data)
{
    SessionContext *asyncContext = static_cast<SessionContext *>(data);
    if (asyncContext->napiSession->session_ != nullptr) {
        int err = CheckIsSupportOpenBasicChannel(asyncContext->napiSession->session_);
        if (err != SE_SUCCESS) {
            ErrorLog("SIM not support open basic channel, err = %{public}d", err);
            asyncContext->errorCode = err;
            return;
        }
        // call native method
        std::string aid = CommonUtils::BytesArrayToHexString(&asyncContext->aid[0], asyncContext->aid.size());
        asyncContext->errorCode = asyncContext->napiSession->session_->OpenBasicChannel(
            aid, asyncContext->p2, asyncContext->channel, asyncContext->response);
        if (asyncContext->channel  == nullptr) {
            ErrorLog("NativeOpenBasicChannel channel is nullptr!");
            asyncContext->resolved = false;
            return;
        }
    } else {
        ErrorLog("NativeOpenBasicChannel napiSession->session_ is nullptr!");
        asyncContext->resolved = false;
        return;
    }

    asyncContext->resolved = true;
}

void NapiSEOmaSession::NativeOpenBasicChannelCallBack(napi_env env, napi_status status, void *data)
{
    DebugLog("enter function NapiSEOmaSession::NativeOpenBasicChannelCallBack");
    SessionContext* asyncContext = static_cast<SessionContext *>(data);
    if (status == napi_ok && asyncContext->resolved && asyncContext->errorCode == ErrorCode::SE_SUCCESS) {
        NapiSEOmaChannel *napiChannel = new NapiSEOmaChannel();
        napiChannel->channel_ = asyncContext->channel;
        napiChannel->session_ = asyncContext->napiSession;
        if (asyncContext->channel == nullptr || asyncContext->napiSession == nullptr) {
            WarnLog("NapiSEOmaSession::NativeOpenLogicChannelCallBack error, channel is null");
        }

        // new instance of JS object
        napi_value channel = nullptr;
        napi_value constructor = nullptr;
        napi_get_reference_value(env, NapiSEOmaChannel::constRef_, &constructor);
        napi_new_instance(env, constructor, 0, nullptr, &channel);
        napi_status status = napi_wrap(
            env, channel, napiChannel,
            [](napi_env env, void *data, void *hint) {
                ErrorLog("NapiSEOmaSession::NativeOpenBasicChannelCallBack delete JsChannel!");
                if (data) {
                    NapiSEOmaChannel* jsChannel = static_cast<NapiSEOmaChannel *>(data);
                    if (jsChannel) {
                        delete jsChannel;
                        jsChannel = nullptr;
                    }
                }
            },
            nullptr, nullptr);
        if (status != napi_ok) {
            if (napiChannel != nullptr) {
                delete napiChannel;
                napiChannel = nullptr;
            }
            WarnLog("napi_wrap failed, object is null. status = %{public}d", status);
        }
        DoAsyncCallbackOrPromise(env, asyncContext, channel);
    } else {
        int errCode = BuildOutputErrorCode(asyncContext->errorCode);
        std::string errorData = asyncContext->response;
        std::string msg = NapiSEOmaCommon::BuildErrorMessage(errCode, "NativeOpenBasicChannelCallBack", "", "", "");
        ThrowAsyncError(env, asyncContext, errCode, msg, errorData);
    }
}

void NapiSEOmaSession::NativeOpenLogicChannel(napi_env env, void *data)
{
    SessionContext *asyncContext = static_cast<SessionContext *>(data);
    if (asyncContext->napiSession->session_ != nullptr) {
        // call native method
        std::string aid = CommonUtils::BytesArrayToHexString(&asyncContext->aid[0], asyncContext->aid.size());
        DebugLog("NapiSEOmaSession::NativeOpenLogicChannel aid = %{public}s, p2 = %{public}d",
            aid.c_str(), asyncContext->p2);
        asyncContext->errorCode = asyncContext->napiSession->session_->OpenLogicalChannel(
            aid, asyncContext->p2, asyncContext->channel, asyncContext->response);
        if (asyncContext->channel  == nullptr) {
            ErrorLog("NativeOpenLogicChannel channel is nullptr!");
            asyncContext->resolved = false;
            return;
        }
    } else {
        ErrorLog("NativeOpenLogicChannel napiSession->session_ is nullptr!");
        asyncContext->resolved = false;
        return;
    }
    asyncContext->resolved = true;
}

void NapiSEOmaSession::NativeOpenLogicChannelCallBack(napi_env env, napi_status status, void *data)
{
    SessionContext *asyncContext = static_cast<SessionContext *>(data);
    if (status == napi_ok && asyncContext->resolved && asyncContext->errorCode == ErrorCode::SE_SUCCESS) {
        NapiSEOmaChannel *napiChannel = new NapiSEOmaChannel();
        napiChannel->channel_ = asyncContext->channel;
        napiChannel->session_ = asyncContext->napiSession;
        if (asyncContext->channel == nullptr || asyncContext->napiSession == nullptr) {
            WarnLog("NapiSEOmaSession::NativeOpenLogicChannelCallBack channel is nullptr!");
        }
        // new instance of JS object
        napi_value channel = nullptr;
        napi_value constructor = nullptr;
        napi_get_reference_value(env, NapiSEOmaChannel::constRef_, &constructor);
        napi_new_instance(env, constructor, 0, nullptr, &channel);
        napi_status status = napi_wrap(
            env, channel, napiChannel,
            [](napi_env env, void *data, void *hint) {
                ErrorLog("NapiSEOmaSession::NativeOpenLogicChannelCallBack delete JsChannel!");
                if (data) {
                    NapiSEOmaChannel* jsChannel = static_cast<NapiSEOmaChannel *>(data);
                    if (jsChannel) {
                        delete jsChannel;
                        jsChannel = nullptr;
                    }
                }
            },
            nullptr, nullptr);
        if (status != napi_ok) {
            if (napiChannel != nullptr) {
                delete napiChannel;
                napiChannel = nullptr;
            }
            WarnLog("napi_wrap failed, object is null. status = %{public}d", status);
        }
        DoAsyncCallbackOrPromise(env, asyncContext, channel);
    } else {
        int errCode = BuildOutputErrorCode(asyncContext->errorCode);
        std::string errorData = asyncContext->response;
        std::string msg = NapiSEOmaCommon::BuildErrorMessage(errCode, "NativeOpenLogicChannelCallBack", "", "", "");
        ThrowAsyncError(env, asyncContext, errCode, msg, errorData);
    }
}

int NapiSEOmaSession::CheckIsSupportOpenBasicChannel(std::weak_ptr<Session> session)
{
    std::string readerName {};
    std::weak_ptr<Reader> reader {};
    if (session.expired()) {
        return SE_ILLEGAL_STATE_ERROR;
    }
    session.lock()->GetReader(reader);
    if (reader.expired()) {
        return SE_ILLEGAL_STATE_ERROR;
    }
    reader.lock()->GetName(readerName);
    InfoLog("CheckIsSupportOpenBasicChannel readerName = %{public}s", readerName.c_str());
    if (readerName.compare(SE_NAME_SIM) == 0) {
        return SE_IO_ERROR;
    }
    return SE_SUCCESS;
}
} // namespace KITS
}  // namespace SE
}  // namespace OHOS
